
namespace Patterns4Net.PatternEnforcer.Core.Patterns
{
    using System.Linq;
    using Mono.Cecil;
    using Patterns4Net.Core.Patterns;
    using Patterns4Net.Core.CecilExtensions;

    [PatternCheckerExport(typeof(Visitor))]
    public class VisitorChecker : PatternCheckerBase<Visitor>
    {
        protected override CheckerResult Check(Visitor pattern)
        {
            if (pattern.IsReflective || pattern.VisitableElements == null)
            {
                return CheckerResult.CreateValidResult();
            }

            var result = new CheckerResultBuilder();
            foreach (var type in pattern.VisitableElements.Where(t => t.IsInterface == false && t.IsAbstract == false))
            {
                DefinesAcceptMethod(type, pattern.VisitorType, result);
            }

            return result.Build();
        }

        private static void DefinesAcceptMethod(TypeDefinition type, TypeDefinition visitorType, CheckerResultBuilder result)
        {
            var acceptMethod = type.Methods.FirstOrDefault(method =>
                             method.HasParameters &&
                             method.Parameters.Any(p => p.ParameterType.IsEqual(visitorType)));
            if (acceptMethod == null)
            {
                result.AddCheckedRule();
                result.Add(
                    "A method that has the Visitor as a parameter was not found in the type {0}. " + 
                    "This method is usually named as Accept, although Pattern Enforcer does not require this name. ",
                    type.Name);
                return;
            }

            // checks that it invokes a method on visitorType supplying itself as a parameter:
            var pom = acceptMethod.GetMethodCalls();
            bool invokesVisit = acceptMethod.GetMethodCalls().Any(call =>
                call.HasTargetObject &&
                call.TargetObject.IsParameter &&
                call.TargetObject.ItemType.IsEqual(visitorType) &&
                call.Method.Parameters.Any(x => 
                    x.ParameterType.IsEqual(type) &&
                    call.Parameters[x.Index].IsThisParameter));
            if (invokesVisit == false)
            {
                result.AddCheckedRule();
                result.Add(
                    "A method {0} declared in the type {1} should invoke Visitor's method with itself as a parameter. ",
                    acceptMethod.Name,
                    type.Name);
            }
        }
    }
}
